fix: review-thread wave 4 — invocation forwarding, codex path regex, tool_input scope, CLI help, README targets, worker-root cleanup, probe redaction/teardown, concurrent doctor scan - #397
Conversation
🦋 Changeset detectedLatest commit: f88e117 The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 23a23c9e88
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| let cap: NodeJS.Timeout | undefined; | ||
| const capped = new Promise<void>((resolvePromise) => { | ||
| cap = setTimeout(resolvePromise, mcpProbePluginDataTeardownCapMs); | ||
| cap.unref(); |
There was a problem hiding this comment.
Keep the cleanup cap alive through shutdown
When a transport teardown stalls and Workbench shuts down before this 10-second cap fires, cap.unref() permits the process to exit without removing the probe's plugin-data directory. The production lifecycle only closes McpProbeRoutes and never awaits mcpProbe.settle(), so the newly detached cleanup has no referenced handle guaranteeing completion; keep this cap referenced or join settle() during server shutdown.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in this PR (5ad8613). Two changes: (1) the 10 s teardown cap in #removePluginDataAfter is now kept referenced (no unref()), so a stalled teardown can no longer let the process exit before the plugin-data removal runs — the timer is still cleared the moment the teardown settles; (2) the dev server's closeForeground now awaits mcpProbe.settle() (in a finally after foreground.close()), so Workbench shutdown joins detached probe cleanups, bounded by that cap. Proof: new integration test mcp-probe-dev-server.test.ts :: joins detached probe plugin-data cleanup into Workbench shutdown starts a real dev server with a stdio transport whose close() takes 400 ms, probes it (response returns while the transport is still closing and plugin data still exists), then asserts server.close() resolves only after the transport close settled and the plugin-data directory is gone. Red without the change, green with it.
There was a problem hiding this comment.
Acknowledged — with cap.unref() and nothing awaiting mcpProbe.settle() on shutdown, a stalled transport teardown can leave the probe's plugin-data directory behind. This PR is still open; owner is fix/review-threads-wave4. Tracked by the late-review sweep; not fixed on main.
commit: |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5ad8613879
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // Probe transports whose teardown outlived their response boundary own | ||
| // their plugin-data removal; joining them here (bounded by the probe's | ||
| // teardown cap) keeps shutdown from leaving those directories behind. | ||
| await mcpProbe.settle(); |
There was a problem hiding this comment.
Wait for active probes before settling teardown
When server.close() is called while a probe is still connecting or listing tools, ForegroundServer only fences new MCP probe requests; it does not await the active handler. At this point McpProbeService.settle() can observe an empty #pendingTeardowns set because cleanup is not registered until #execute reaches its finally, so this close resolves and the probe later creates a detached teardown and plugin-data cleanup. Include the in-flight probe promises in the settlement fence so shutdown cannot finish while their transports and temporary directories remain active.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in this PR (859f375). McpProbeService.settle() now fences the service's in-flight probe promises (#inFlight) as well as #pendingTeardowns, looping until both are empty — so server.close() cannot resolve while a probe is still connecting/listing tools; once that probe reaches its response boundary it registers its teardown, which the same loop then awaits (bounded by the 10 s teardown cap). Proof: new unit test mcp-probe-service.test.ts :: settle() fences in-flight probes, not only already-registered teardowns (#397 review) blocks a probe inside connect(), calls settle(), asserts it is still pending 50 ms later, releases the connect, and asserts settle resolves only after the report returned, the (slow) transport closed, and the plugin-data directory is gone. Red on the previous commit, green now.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 859f37518b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| 'command is generated; add `claude`, `codex`, or `cursor` to `targets` in', | ||
| '`agent-bundle.config.ts` to generate one.', |
There was a problem hiding this comment.
Document the package bin change needed to enable installers
For a portable-only scaffold, rewriteManifest removes the installer entry from package.json; adding claude, codex, or cursor only to agent-bundle.config.ts later does not restore that npm bin mapping. The build may emit the installer file, but the documented npx command remains unavailable after publishing, so these generated instructions should also tell users to restore the corresponding package.json entry (or preserve a mechanism that regenerates it).
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in this PR (c53c000). The portable-only README rewrite in rewriteReadmeInstall now states that the installer's bin entry was dropped from package.json and names the exact mapping to restore — "<installer-bin>": "./dist/bin/<installer-bin>.js" — in both the shell-comment example and the prose, alongside the config-target change, and notes that the build emits the installer file but never edits the manifest. Test: scaffold.test.ts :: renders README install instructions for the selected targets now derives the dropped entry from the template's shipped package_json bin map (after plugin-name substitution) and asserts the portable-only README contains it in both places, so template drift in the bin path fails the test.
| const teardown = Promise.allSettled([client.close(), transport.close()]); | ||
| const cleanup = this.#removePluginDataAfter(teardown, options.pluginData); |
There was a problem hiding this comment.
Protect cleanup from synchronous close failures
If either injected or SDK transport's close() throws synchronously, evaluation of the array passed to Promise.allSettled aborts before this cleanup is registered (and a throwing client.close() also skips transport.close()). Because #run no longer has its former outer finally, that scenario turns the probe into a rejection and permanently leaks its plugin-data directory; wrap each close invocation so synchronous throws become settled promises and always reach #removePluginDataAfter.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in this PR (c53c000). Added a module-level settledClose(close) that invokes the callback inside try/catch and normalizes both a synchronous throw and a rejection into one settled promise. #execute's finally now builds the teardown as Promise.allSettled([settledClose(() => client.close()), settledClose(() => transport.close())]), so a throwing client.close() neither skips transport.close() nor aborts before #removePluginDataAfter is registered; the #withinBudget timeout paths use the same wrapper for onTimeout. Proof (both red before, green now): mcp-probe-service.test.ts :: still closes the transport and removes plugin data when a close() throws synchronously (#397 review) (client close throws synchronously → report is still ok, transport closed, plugin data removed after settle()), and :: reports a timeout even when the timeout teardown throws synchronously (spent-budget connect path with a synchronously throwing transport close → honest timed-out/connect report, plugin data removed).
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c53c00090f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| */ | ||
| const hasAbsolutePath = (value: string): boolean => | ||
| /(?:file:|(?:^|[\s"'([{=,:])\/[^\s,;{}()[\]<>"']+|(?:^|[\s"'([{=,:])[A-Za-z]:[\\/]|\\\\)/u.test(value); | ||
| /(?:file:|(?:^|[\s"'([{=,]|:(?!\/\/))\/[^\s,;{}()[\]<>"']+|(?:^|[\s"'([{=,:])[A-Za-z]:[\\/]|\\\\)/u.test(value); |
There was a problem hiding this comment.
Redact URL userinfo before exempting links
When a probed server returns instructions, titles, or tool descriptions containing an HTTP URL with Basic-auth userinfo (for example, https://alice:hunter2@example.test/private), redactCredentialText does not recognize the password and this new :// exemption makes hasAbsolutePath return false, so the complete credential is emitted in the browser-facing probe report. Preserve ordinary documentation URLs, but strip or reject URL userinfo before allowing the URL through.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in this PR (0761d34). redactProbeText now masks URL userinfo before the absolute-path check: urlUserinfoPattern = /\b([a-z][a-z0-9+.-]*:\/\/)[^\s/?#@"'<>]+@/giu rewrites scheme://user[:secret]@host… to scheme://[REDACTED]@host… for every scheme (not just http/https), while a plain email address (no ://) and ordinary documentation URLs pass through unchanged. Test: extended mcp-probe-service.test.ts :: keeps URLs while redacting real absolute and bundle paths (#316 review) with https://alice:hunter2@example.test/private, postgres://svc:pa55@db.internal:5432/app, a bare-user https://alice@example.test/private, and an email ops@example.test; asserts the masked forms and that the serialized report contains none of hunter2, pa55, alice. Red on the previous commit (the credential appeared verbatim), green now.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0761d34f71
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| return rm(pluginData, { force: true, recursive: true }); | ||
| }) | ||
| .then(() => undefined, () => undefined); |
There was a problem hiding this comment.
Retry cleanup after a capped teardown settles
When a transport remains alive past the 10-second cap and still holds files under pluginData—notably on Windows—the cap wins this race and rm can fail with EPERM; the following rejection handler converts that failure to success, and nothing retries removal if the transport later finishes closing. Consequently settle() and Workbench shutdown can report completion while leaving the probe directory behind. Preserve a retry chained to the eventual teardown settlement rather than permanently swallowing the capped removal failure.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in this PR (e85271d). #removePluginDataAfter now records whether the cap won the race; if it did and the removal then fails, one more rm is chained to the teardown's eventual settlement and tracked in #pendingTeardowns via a new #track helper, so settle() (and therefore Workbench shutdown) does not report completion while the directory is still held — the failure is no longer swallowed for good. Removal itself uses rm({ force, recursive, maxRetries: 3, retryDelay: 50 }) so transient EPERM/EBUSY are retried in place. A pluginDataTeardownCapMs option was added as a testing seam (production keeps the 10 s constant). Test: mcp-probe-service.test.ts :: retries plugin-data removal once a capped teardown finally settles (#397 review) — plugin data under a read-only parent stands in for the held directory, cap = 100 ms, transport close stalls; asserts the first removal fails (proof file still present after 250 ms), settle() stays pending, and once the parent is writable and the close is released the fence resolves only after the chained retry removed the directory. Red on the previous commit, green now.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e85271d19f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| }) | ||
| .then(() => undefined, () => { | ||
| if (!capWon) return; | ||
| this.#track(teardown.then(() => removePluginData(pluginData)).then(() => undefined, () => undefined)); |
There was a problem hiding this comment.
Bound the cleanup retry tracked by shutdown
When the teardown cap fires, removePluginData fails (for example with EPERM on Windows), and the faulty transport never settles, this newly added retry remains in #pendingTeardowns forever because it waits directly on teardown. The fresh evidence is that closeForeground() now awaits mcpProbe.settle(), whose loop cannot finish while this retry remains tracked, so the cap no longer bounds Workbench shutdown in the exact stalled-close scenario it is intended to guard; bound or otherwise release the retry while preserving best-effort eventual cleanup.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in this PR (90772c0). The retry chained to the teardown's eventual settlement is now detached from the settle() fence (it is no longer added to #pendingTeardowns): settle() — and therefore closeForeground() — stays bounded by the cap even when a faulty transport never settles, while the retry still removes the directory best-effort if the transport does eventually close. The doc comment on #removePluginDataAfter states this trade-off explicitly. Test updated: mcp-probe-service.test.ts :: retries plugin-data removal once a capped teardown finally settles (#397 review) now asserts that settle() resolves within 500 ms while the transport is still stalled and the held directory is still present (a fence that waited on the transport fails the test), and that after the transport is released the chained retry removes the directory (event order settled → transport-closed → plugin-data-removed).
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 90772c0a76
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| createPluginData: async () => { | ||
| await mkdir(pluginData); | ||
| await writeFile(join(pluginData, 'proof.txt'), 'present'); | ||
| await chmod(parent, 0o555); |
There was a problem hiding this comment.
Avoid relying on mode bits to force removal failure
When the unit suite runs as root in a container, or on a platform such as Windows where POSIX directory mode bits are not enforced equivalently, chmod(parent, 0o555) does not guarantee that rm(pluginData, { recursive: true }) will fail. The capped cleanup can therefore delete proof.txt, making the assertion at line 651 fail even though the implementation behaves correctly; simulate the removal rejection through an injected seam or otherwise use a platform-independent failure mechanism.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in this PR (b3af919). Added a removePluginData testing seam to McpProbeServiceOptions (production default is the recursive rm) and rewrote the test to inject the failure: while the fake transport is alive the injected removal throws an EPERM error, and after the transport closes it delegates to the real rm. No mode bits are involved, so the failure is identical as root, in containers, and on Windows. The test now also asserts the event order removal-rejected → settled → transport-closed → plugin-data-removed.
| expect(rstestWorkerRootOwner(root)).toMatchObject({ | ||
| cwd: process.cwd(), | ||
| pid: process.pid, | ||
| temporaryRoot: expect.stringMatching(/^\//u), |
There was a problem hiding this comment.
Make the owner-marker assertion path-portable
When this test runs on Windows, os.tmpdir() records a drive-letter or UNC path such as C:\Temp in temporaryRoot, while this assertion unconditionally requires the value to begin with /. The production helper explicitly supports win32, so the unit suite will fail on that supported path shape; assert with path.isAbsolute() or use a platform-aware matcher.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in this PR (b3af919). The assertion now uses path.isAbsolute(owner.temporaryRoot) instead of /^\//, so drive-letter (C:\Temp) and UNC roots on Windows satisfy it alongside POSIX /tmp; the rest of the marker (cwd, pid, workerId, and temporaryRoot !== root) is asserted as before.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b3af91921a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // Same contract as the timer path below: the transport's own close | ||
| // (TERM/KILL for stdio) keeps running, but a stalled close never holds | ||
| // the timed-out report — #execute's bounded teardown owns that wait. | ||
| void settledClose(onTimeout); |
There was a problem hiding this comment.
Track the first timeout close before cleaning plugin data
When the budget is already exhausted and the first transport.close() remains pending, this fire-and-forget promise is not included in #execute's teardown fence. #execute immediately invokes close() again; for a non-reentrant transport whose duplicate close returns early, that second promise lets plugin-data removal run while the original TERM/KILL sequence is still active. On Windows the removal can then fail with capWon === false, so it is neither retried nor reported. Reuse or otherwise pass the first close promise into the bounded teardown so cleanup follows the actual close without delaying the probe response.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in this PR (acc03ce). #execute now owns a single memoized closeTransport() (transportClose ??= settledClose(() => transport.close())); both #withinBudget timeout hooks (connect and protocol) and the teardown in finally call it, so the teardown fence — and the plugin-data removal chained behind it — awaits the very close a timeout already started rather than a duplicate call a non-reentrant transport answers immediately. Test: mcp-probe-service.test.ts :: chains plugin-data removal to the close a timeout already started, not a duplicate close (#397 review) — spent-budget connect path, transport whose first close stalls and whose duplicate close returns at once; asserts close() was invoked exactly once, the plugin data survives while the first close is pending, and it is removed after that close is released and settle() resolves. Red on the previous commit (the duplicate close let removal run early), green now.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: acc03ce308
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| * credential redaction does not recognize; because URLs are exempt from the | ||
| * absolute-path fail-closed rule, the userinfo is stripped before that check. | ||
| */ | ||
| const urlUserinfoPattern = /\b([a-z][a-z0-9+.-]*:\/\/)[^\s/?#@"'<>]+@/giu; |
There was a problem hiding this comment.
Redact userinfo through the final @ delimiter
When a server returns a URL whose password contains a raw @, such as https://alice:pa@ss@example.test/private, WHATWG URL parsing treats the final @ as the authority delimiter and the earlier one as part of the password. This pattern stops at the first @, producing https://[REDACTED]@ss@example.test/private and exposing part of the credential in the browser-facing report; excluded characters such as quotes can prevent matching altogether despite also being accepted and encoded by URL parsers. Fresh evidence in the replacement regex is its explicit exclusion of @ and other valid userinfo characters, so redact through the final authority delimiter or parse URLs before masking.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in this PR (4ee7483). urlUserinfoPattern is now /\b([a-z][a-z0-9+.-]*:\/\/)[^\s/?#\\]*@/giu: the authority run excludes only whitespace and the WHATWG authority terminators (/, ?, #, \) and is greedy, so the match extends through the final @ before the path — the delimiter URL parsers honour — and no longer excludes @, quotes, or other characters parsers accept in userinfo. An @ in the query/fragment is untouched. Test: added https://alice:pa@ss@example.test/private?next=me@x and https://al"ice:s3cret@example.test/#top to :: keeps URLs while redacting real absolute and bundle paths (#316 review); asserts both become https://[REDACTED]@example.test/... with ?next=me@x preserved, and that the serialized report contains none of pa@ss, @ss@, s3cret. With the previous pattern the test reproduces exactly the leak you described (https://[REDACTED]@ss@example.test/... and the quoted credential verbatim); green now.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4ee7483262
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| * `@`, the delimiter URL parsers honour, so a raw `@` or quote inside a | ||
| * password cannot leave part of the credential behind. | ||
| */ | ||
| const urlUserinfoPattern = /\b([a-z][a-z0-9+.-]*:\/\/)[^\s/?#\\]*@/giu; |
There was a problem hiding this comment.
Redact backslashes in non-special URL userinfo
When probe text contains userinfo in a non-special URL such as postgres://alice:se\cret@example.test/db, WHATWG parsing accepts the backslash as part of the password, but this pattern stops at \ before reaching @ and performs no masking; hasAbsolutePath also does not reject a single backslash, so the full credential reaches the browser-facing report. Fresh evidence beyond the prior userinfo fix is the final regex's explicit \ exclusion, which is valid as an authority terminator for HTTP(S) but not for schemes such as the already-supported postgres:// example.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in this PR (281520f). \ is no longer an authority terminator in urlUserinfoPattern — now /\b([a-z][a-z0-9+.-]*:\/\/)[^\s/?#]*@/giu, stopping only at whitespace or the terminators every WHATWG scheme shares (/, ?, #). For non-special schemes the backslash is userinfo and is masked; for special schemes this errs toward redaction, which is the safe direction for a browser-facing report. The doc comment records that trade-off. Test: added postgres://alice:se\cret@example.test/db and https://bob:pw\x@example.test/ to :: keeps URLs while redacting real absolute and bundle paths (#316 review); asserts both become scheme://[REDACTED]@example.test/... and that the serialized report contains neither cret nor bob. With the previous pattern the test fails with the credential verbatim; green now.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 281520fc47
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const hasAbsolutePath = (value: string): boolean => | ||
| /(?:file:|(?:^|[\s"'([{=,:])\/[^\s,;{}()[\]<>"']+|(?:^|[\s"'([{=,:])[A-Za-z]:[\\/]|\\\\)/u.test(value); | ||
| /(?:file:|(?:^|[\s"'([{=,]|:(?!\/\/))\/[^\s,;{}()[\]<>"']+|(?:^|[\s"'([{=,:])[A-Za-z]:[\\/]|\\\\)/u.test(value); |
There was a problem hiding this comment.
Redact paths hidden behind local URI schemes
When probe text contains a local-resource URI such as unix:///home/alice/private.sock or vscode://file/home/alice/project, every slash in the absolute path follows either : from :// or another slash, so this expression returns false and the full machine-local path reaches the browser-facing probe report. Preserve documentation URLs by exempting only appropriate network schemes such as HTTP(S), or explicitly classify local-resource URI paths before applying the exemption.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in this PR (242add5). The URL exemption is now limited to network schemes: a new localUriPathPattern = /\b(?!(?:https?|wss?):\/\/)[a-z][a-z0-9+.-]*:\/\/[^\s/]*\//iu makes hasAbsolutePath return true for any other scheme://<authority>/… — so unix:///home/alice/private.sock, vscode://file/home/alice/project, postgres://host/db, and file: all fail closed like a bare absolute path, while oci://registry.example.test (no path component) and mailto: are untouched. Userinfo masking still runs first for every scheme. Test: :: keeps URLs while redacting real absolute and bundle paths (#316 review) now has fixtures for the empty-authority form, the authority-then-path form, a non-network scheme with a path, and a non-network scheme without one; asserts the first three are [REDACTED] and the last is preserved, and that alice never appears in the serialized report. Red on the previous commit (both local URIs reached the report verbatim), green now.
| .then(() => undefined, () => { | ||
| if (!capWon) return; | ||
| void teardown.then(() => this.#removePluginData(pluginData)).catch(() => undefined); |
There was a problem hiding this comment.
Retry removal after an early close failure
When close() throws or rejects while a Windows child still briefly holds pluginData, settledClose makes the teardown appear complete, the immediate rm can fail with EPERM, and this branch discards that failure because the cap did not win; settle() then completes with the directory permanently left behind. Fresh evidence in the current code is the explicit if (!capWon) return, so schedule a bounded delayed retry for this early-failure path as well.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in this PR (242add5). In the !capWon branch (teardown settled — possibly because a close failed fast — but the removal still failed), #removePluginDataAfter now schedules one bounded retry after mcpProbePluginDataRetryDelayMs (250 ms) and tracks it via #track in #pendingTeardowns, so settle() / Workbench shutdown waits for that retry and the fence stays bounded. The capped-and-stalled branch keeps its best-effort, unfenced retry as discussed on the earlier thread. Test: mcp-probe-service.test.ts :: retries removal once, fenced, when the teardown settled but the directory was still held (#397 review) — transport close rejects immediately, the injected removal throws EPERM on the first call and delegates to rm on the second; asserts settle() resolves only after two removals and the directory is gone. Red on the previous commit (one removal, directory left behind), green now.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 242add56f8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| * authority for special schemes, while non-special ones such as | ||
| * `postgres://` accept it inside userinfo — masking errs toward redaction. | ||
| */ | ||
| const urlUserinfoPattern = /\b([a-z][a-z0-9+.-]*:\/\/)[^\s/?#]*@/giu; |
There was a problem hiding this comment.
Redact whitespace-bearing URL userinfo
When a probed server returns a valid URL whose password contains raw whitespace, such as https://alice:se cret@example.test/private, WHATWG parsing percent-encodes the space (and strips embedded tabs/newlines), but this pattern stops at \s before reaching @ and performs no masking. The absolute-path check also exempts the URL, so the complete credential is emitted in the browser-facing probe report. Fresh evidence beyond the earlier delimiter fixes is the final pattern's explicit \s exclusion; parse the URL or allow whitespace through the authority match before masking.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in this PR (399122c). urlUserinfoPattern is now /\b([a-z][a-z0-9+.-]*:\/\/)[^/?#]*@/giu: the authority run stops only at the three terminators every WHATWG scheme shares (/, ?, #) and is greedy through the final @, so whitespace (encoded or stripped by parsers) — like @, quotes and \ before it — can no longer end the mask short of the credential. The doc comment now states the deliberate consequence: a path-less URL followed by an @ before any /, ?, # is over-masked as if it were userinfo, which is the safe side for a browser-facing report. Test: :: keeps URLs while redacting real absolute and bundle paths (#316 review) gains https://alice:se cret@example.test/private and a tab-bearing https://bob:x\ty@example.test/ (both → https://[REDACTED]@example.test/…), plus a fixture pinning the documented over-masking trade-off. Red on the previous pattern (the space-bearing credential appeared verbatim), green now.
|
@codex review |
|
Codex Review: Didn't find any major issues. More of your lovely PRs please. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
…gex, tool_input scope, CLI help, README targets, worker-root cleanup, probe redaction/teardown
…nvas silence claim; changeset
…down Keep the probe teardown cap referenced and await McpProbeService.settle() from the dev server close path so a stalled stdio teardown can no longer leave a plugin-data directory behind when Workbench shuts down.
A probe registers its detached teardown only at its response boundary, so a settle fence over teardowns alone let Workbench shutdown resolve while a still-connecting probe held a transport and plugin-data directory. settle() now also awaits in-flight probes.
…document restoring installer bin McpProbeService wraps each close() so a synchronous throw becomes a settled promise: the other close still runs and plugin-data removal is always registered. Portable-only scaffold READMEs now name the package.json bin entry that must be restored alongside the config target to re-enable the installer command.
URLs are exempt from the probe report's absolute-path fail-closed rule, so Basic-auth userinfo (scheme://user:secret@host) would have reached the browser-facing report verbatim. It is masked as scheme://[REDACTED]@host before that check.
…ttles When the teardown cap wins and the removal fails because the transport still holds the directory (Windows EPERM), chain one more removal to the teardown's eventual settlement and fence it in settle() instead of swallowing the failure. Adds a pluginDataTeardownCapMs testing seam.
The retry chained to a stalled teardown is best-effort: tracking it would let a transport that never settles hold Workbench shutdown open, which is the case the cap exists to bound.
…m-independent Inject the plugin-data removal through a testing seam instead of relying on directory mode bits, and assert the owner marker's temporaryRoot with path.isAbsolute() so the suite holds as root, in containers, and on Windows.
…down A budget timeout starts transport.close() early; the teardown now awaits that same memoized promise instead of a duplicate close a non-reentrant transport answers immediately, so plugin-data removal follows the real TERM/KILL path.
The userinfo match now runs greedily to the last @ before a WHATWG authority terminator, so a raw @ or quote inside a password cannot leave part of the credential in the probe report.
Only /, ?, and # end the authority for every WHATWG scheme; a backslash is valid userinfo for non-special schemes such as postgres://, so it no longer stops the mask short of the credential.
…n-data removal Only http(s)/ws(s) URLs are exempt from the probe report's absolute-path rule; unix:///, vscode://file/... and other scheme://.../ forms fail closed. When the teardown settles but the first removal fails (Windows still holding the directory), one bounded, fenced retry follows.
URL parsers percent-encode spaces and strip tabs/newlines inside userinfo, so whitespace no longer ends the authority run; only /, ?, and # do. A path-less URL followed by an @ before those terminators is over-masked by design.
|
@codex review |
399122c to
5ebf740
Compare
|
@codex review |
1 similar comment
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5ebf7403a7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| plugin schema rejects line terminators in component, interface-asset, and | ||
| screenshot paths so a newline followed by `/../` cannot slip past the traversal | ||
| guard (codex adapter 1.8.0, plugin adapter 1.23.0). Any-JSON `permission/request` |
There was a problem hiding this comment.
Rewrite the changeset summary to match release policy
This publishable-package changeset records Codex/plugin adapter revisions as 1.8.0/1.23.0 even though this commit sets them to 1.9.0/1.24.0, and the implementation-focused summary neither names the relevant diagnostic codes nor ends with the required (#PR) suffix. Rewrite it as an accurate user-facing release summary before it becomes published changelog text.
AGENTS.md reference: AGENTS.md:L27-L28
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in this PR (461e87c). Rewrote .changeset/review-threads-wave4.md per AGENTS.md: a user-facing imperative summary ending with (#397), correct adapter revisions (Codex 1.9.0, composite plugin 1.24.0 — bumped again after the rebase re-pinned the schema on top of #392), the diagnostic codes a rejected manifest now hits (AB6012, AB6032), the affected commands/exports/config surfaces (agent-bundle build|prepack --help --output, agent-bundle doctor, Workbench server.close(), create-agent-bundle --targets), and the dropped claim about rendered-worker invocation forwarding (that landed in #366, not here). Still exactly one changeset in the PR; Changeset present stays green.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 461e87c36c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| * `file:` — may carry a machine-local path in its authority or path and fails | ||
| * closed like a bare absolute path. | ||
| */ | ||
| const localUriPathPattern = /\b(?!(?:https?|wss?):\/\/)[a-z][a-z0-9+.-]*:\/\/[^\s/]*\//iu; |
There was a problem hiding this comment.
Remove the word-boundary requirement from URI redaction
When MCP metadata concatenates a URL after an identifier ending in a digit or underscore, such as _https://alice:hunter2@example.test/private or _unix:///home/alice/private.sock, the leading \b is false, so this local-URI check and the corresponding urlUserinfoPattern both skip the URL; the fallback absolute-path expression also exempts the :// slashes, allowing the credential or machine-local path into the browser-facing report. The fresh remaining case is the shared word-boundary requirement, so match schemes without depending on the preceding character.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in this PR (f88e117). Both localUriPathPattern and urlUserinfoPattern drop the \b and instead anchor the scheme to the start of its own character run with (?<![a-z0-9+.-]) — a plain removal of \b would have let the local-URI rule start mid-scheme (ttps://… inside https://…) and redact every link. The network-scheme exemption now looks through any glued prefix ((?![a-z0-9+.-]*(?:https?|wss?):\/\/)), so _https://alice:hunter2@… and ref9https://user:pw@… are masked, sock_unix:///home/… fails closed, and a glued ref9https://example.test/docs link still survives. Covered by three new fixtures in tests/mcp-probe-service.test.ts (url-userinfo-after-identifier, local-uri-after-identifier, network-url-after-identifier) plus the serialized-report secret sweep.
|
@codex review |
|
Codex Review: Didn't find any major issues. Hooray! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Summary
Closes out the 11 unreplied review threads on merged PRs #316, #317, #319, #321, #324, #341, #364, #365 (the #362 P1 is owned by the #99 lane and is not touched here).
invocation(landed via fix(cli): mount request context providers for plain routed CLI commands (#313 audit) #366; this PR adds the projected-MCP-command coverage and a harness test that the render message carries the invocation, so providers branching oninvocation.kindare exercised for rendered CLI, projected MCP command, and rendered script).plugin.schema.jsoncomponent/interface-asset/screenshot patterns reject line terminators (\n \r \u2028 \u2029) and scan every character for..; the screenshots pattern also rejects./assets/../. PROVENANCE re-pinned (sha/bytes), codex adapter 1.8.0 / plugin 1.23.0, negative fixtures with embedded newlines incodex-plugin-validationandhost-adapters.permission/requesttool_inputis Codex-only; Claude stays object-shaped. Unit + route-unit projection tests for both targets.build --help/prepack --helpsaydefault artifact(both commands build with package outputs).docs/framework-mode.mdupdated; CLI help test.install <host>line per installable selected target (or explain that no installer bin exists for portable-only scaffolds); drift-checked markers; scaffold test across default / cursor-only / plugin / portable / minimal./tmp/ab-rstest-<hash>worker root carries an owner marker naming the hostTMPDIRand pid;scripts/local-ci.mjsremoves the roots owned by a leg's TMPDIR before and after the leg (only those, only when the owning process has exited) viascripts/rstest-worker-roots.mjs. Test covers finished/interrupted/live/other-owner/unmarked/corrupt roots.://as a path separator; URLs survive, real absolute paths (incl.cwd:/xandfile:) still fail closed.McpProbeService.settle()awaits detached teardowns. Also: the exhausted-budget path no longer awaits a stalled close. Ordering test proves report → transport close → rm.mapConcurrent), stitching results back in directory order; test with 6 silent listeners finishes in ~1 s (serial baseline times out at 5 s).tool/beforeand namesstop/prompt/submitas decision-capable-yet-silent families; the managed canvas copy was updated identically (cmpclean).53bc86f31);api.test.ts/plugin-bundle.test.tsfixtures are marketplace-qualified and green.Evidence
pnpm typecheck,pnpm lintgreen after rebase ontoorigin/main(9fe8a35).pnpm test:route-unit(36) andpnpm test:projection(63) green.pnpm test:unit: 2685–2687 pass; the 1–3 failures per run rotate across unrelated files (inspect-state,dispatcher,native-claude-contract,playground-service,native-playground-service) and each passes when rerun alone — the host was at load average 100–160 from concurrent lanes. CI is authoritative.pnpm build && pnpm test:integration:runpre-rebase: 938 pass, 1 fail (host-install-proofCodexlogokey) which test(install): expect the Codex interface.logo field in the packed host-install proof #367 fixed on main; post-rebase run in progress../a\n/../../outsidebefore the change.Test plan